課程:JavaScript 與 React 底層原理 第 19 堂:Hooks 底層收尾
Lesson 19 note
本次課程完整收尾了 Topic 10,深入探討了 useReducer 的底層環形鏈結串列機制與狀態機思維,並學習了如何透過自訂 Hook 封裝複雜邏輯與副作用。透過最後的總結複習,將 Hooks 的鏈結串列結構、閉包陷阱與參考型別等核心概念串聯起來,建立了完整的知識體系。
useReducer 底層邏輯
- useState 本質上是 useReducer 的語法糖
- 底層透過
basicStateReducer來處理更新,這解釋了為什麼setCount既能接收值也能接收函數。
- 底層透過
- 狀態更新的底層機制:環形鏈結串列(Circular Linked List)
mountReducer階段會建立hook.queue,並採用環形鏈結串列結構儲存 pending 的 actions。
updateReducer階段會遍歷這個佇列,依序計算狀態演進(State A -> State B -> Final State),保證更新的順序性與完整性。- dispatch 函數的穩定性
- dispatch 透過閉包捕獲了 Fiber 節點與 queue 引用,因此在組件整個生命週期中參考不變,適合向下傳遞給子組件而不觸發額外渲染。
- 從數據驅動轉向「意圖驅動」的狀態機思維
- 將「如何更新」的邏輯從 UI 抽離至 Reducer 純函數中,避免分散的
useState導致非法狀態組合,提升程式碼的可測試性與解耦程度。
- 將「如何更新」的邏輯從 UI 抽離至 Reducer 純函數中,避免分散的
自訂 Hook 設計
- 自訂 Hook 的物理本質:普通函數 + Hooks 規則 + 邏輯封裝
- 它並非 React 內建的硬編碼功能,而是利用函數封裝來達成邏輯複用。
- 底層結構的「扁平化」展開
- 在 Fiber 節點眼中,自訂 Hook 內部的 Hooks 會按順序展開並鏈結在該組件的
memoizedState串列中,這也是為什麼自訂 Hook 內部也必須遵守不可在條件式中呼叫的規則。
- 在 Fiber 節點眼中,自訂 Hook 內部的 Hooks 會按順序展開並鏈結在該組件的
- 狀態與副作用的抽象模式
- 狀態抽象(如
useInput):捕獲變數變更過程,讓 UI 組件變「笨」,專注於展示。
- 狀態抽象(如
- 副作用抽象(如
useFetch):封裝複雜的清理(Cleanup)與邊界處理邏輯,讓組件不需關心實作細節。 - 設計最佳實踐
- 返回值設計:簡單通用邏輯用「陣列解構」;複雜且具多屬性的邏輯優先使用「物件解構」以利後續擴充。
- 封裝時機:當出現邏輯複用、複雜度過高需隔離或有獨立測試需求時,就是抽離 Hook 的黃金信號。
Q&A
- Q: 在同一個組件中呼叫兩次同一個自訂 Hook(例如兩個計時器),React 如何確保它們的狀態不會互相干擾?
- 關鍵在於 Fiber 節點內部的「鏈結串列」與「指針」機制。
- 自訂 Hook 在執行時會被「扁平化」展開。當程式碼執行到第二次呼叫時,React 內部的指針會向後移動,在該 Fiber 節點的 Hooks 鏈結串列中依序建立全新的 Hook 物件。
- 因為它們在記憶體中佔據的是鏈結串列上不同的位址,所以各自擁有的 state 是完全獨立的。
- Q: 為什麼
useRef能夠避開 useEffect 或 setTimeout 中常見的「過時閉包(Stale Closure)」問題?- 這是利用了 JavaScript 中「參考型別(Reference Type)」與「地址捕獲」的特性。
- 基本型別(如數字)在閉包形成時捕獲的是「值的快照」,因此會困在過去。
useRef回傳的是一個物件,閉包捕獲的是該物件的「記憶體地址」。無論經過多久,只要透過該地址讀取內部屬性(current),就能獲取到該物件當前最新的值。- Q: 為了讓
useReducer完美模擬useState的行為,basicStateReducer內部該如何實作?- 核心邏輯在於對 action 型別的判斷。
- 實作方式為:
function basicStateReducer(state, action) { return typeof action === 'function' ? action(state) : action; }。 - 這樣就能同時相容直接傳入新值或傳入一個更新函數(updater function)的兩種常見用法。